package il.ac.tau.arielgue.outxing.contexts;

import il.ac.tau.arielgue.outxing.agents.OutXingOrganism;
import il.ac.tau.arielgue.outxing.agents.UniformOrganism;
import il.ac.tau.arielgue.outxing.utils.CsvWriter;
import il.ac.tau.arielgue.outxing.utils.Params;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import proevolution.AbstractStructuredContext;
import proevolution.agents.Organism;
import proevolution.utils.DateUtils;
import repast.simphony.context.Context;
import repast.simphony.engine.schedule.ScheduleParameters;
import repast.simphony.engine.schedule.ScheduledMethod;
import repast.simphony.random.RandomHelper;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimap;

/**
 * TODO : deserialize demes...
 * 
 * @author arielgue
 * 
 */
public class OutXingContext extends AbstractStructuredContext {

	private static final long serialVersionUID = 8719634878018008203L;

	private PoolSubContext pool;
	private transient CsvWriter poolWriter;
	private List<DemeSubContext> demes = null;

	/*
	 * using tools.Params. so this is probably not needed anymore. saving it
	 * around just to make sure. private double migrationFitnessCost; private
	 * double inoutXingFitnessCost; private double heterozygotCoefficient;
	 * private double demeMaxPopulationModifier = 1; private double
	 * demeMaxPopulationPeriod = 100; private int demeCarryingCapacity;
	 */

	public OutXingContext() {
		super();
	}

	public OutXingContext(Object name, Object typeID) {
		super(name, typeID);
	}

	public OutXingContext(Object name) {
		super(name);
	}

	/**
	 * add an organism to a random deme.
	 * 
	 * @param organism
	 * @return returns the id of the deme to which the organism was added, or
	 *         null if failed adding to a deme.
	 */
	public Object addToRandomDeme(Organism organism) {
		return addToOtherRandomDeme(organism, -1);
	}

	public Object addToOtherRandomDeme(Organism organism, int notToThisDemeId) {
		int id = notToThisDemeId;
		while (id == notToThisDemeId) {
			id = RandomHelper.nextIntFromTo(0, getDemes().size() - 1);
		}

		Context<Organism> deme = getDemes().get(id);
		if (deme.add(organism)) {
			return id;
		} else {
			return null;
		}
	}

	/**
	 * add an organism to a the pool
	 * 
	 * @param organism
	 * @return <i>true</i> if this collection changed as a result of the call
	 */
	public boolean addToPool(Organism organism) {
		return pool.add(organism);
	}

	/*
	 * TODO: remove this method if sure it is not called anywhere
	 */
	public void methodsToExecuteBeforeLastPriorityToIncludeInStatistics() {

	}

	/**
	 * the effect of predators can be overcome only when conditions are good
	 * (but not too good), else it is another cost
	 */
	// @ScheduledMethod(start = 1, interval = 1, priority = 14)
	private void demesPredatorsEffect() {
		for (DemeSubContext deme : getDemes()) {
			deme.predatorsEffect();
		}
	}

	// @ScheduledMethod(start = 1, interval = 1, priority = 11)
	public void outXing() {
		for (DemeSubContext deme : getDemes()) {
			pool.addAll(deme.outcross());
		}
	}

	/**
	 * called by the schedule. does random migration of the organisms in the
	 * demes. we collect all organisms that are destined to migrate, recalling
	 * which deme they came from, and them add them to random demes - but not to
	 * the deme they came from.
	 */
	// @ScheduledMethod(start = 1, interval = 1, priority = 9)
	public void migration() {
		Multimap<Integer, Organism> migrators = HashMultimap.create();

		for (int demeId = 0; demeId < getDemes().size(); demeId++) {
			DemeSubContext deme = getDemes().get(demeId);
			for (Organism o : deme.migrate()) {
				migrators.put(demeId, o);
			}
		}

		for (int demeId : migrators.keySet()) {
			for (Organism o : migrators.get(demeId))
				addToOtherRandomDeme(o, demeId);
		}
	}

	// @ScheduledMethod(start = 1, interval = 1, priority = 7)
	public void demesCreateNewGeneration() {
		for (DemeSubContext deme : getDemes()) {
			deme.createNewGeneration();
		}
		pool.createNewGeneration();
	}

	/**
	 * all the step-methods are either included in the statistics, or excluded
	 * as LAST_PRIORITY
	 */

	@ScheduledMethod(start = 2, interval = 1, priority = 12)
	public void stepsToExecuteBeforeLastPriorityAndIncludedInStatistics() {
		demesPredatorsEffect();
		migration();
		outXing();

	}

	@ScheduledMethod(start = 1, interval = 1, priority = 6)
	// TODO change interval to 100? 1000? according with the changes in the
	// aggregate Repast statistic methods
	public void extraStatistics() {
		getPoolWriter().writeCell(DateUtils.getCurrentTick()-1);
		getPoolWriter().writeCell(getPool().size());
		getPoolWriter().writeCell(getPool().getMeanFitness());
		getPoolWriter().writeCell(getPool().getMeanHeterozygosity());
		getPoolWriter().writeCell(getPool().getMeanDeleteriousHomozigosity());

		// until debugging the dataGatheres is done, use:
		getPoolWriter().writeCell(this.getUniMeanHeterozygosity());
		getPoolWriter().writeCell(this.getUniMeanHomozygosity());
		getPoolWriter().writeCell(this.getFaoMeanHeterozygosity());
		getPoolWriter().writeCell(this.getFaoMeanHomozygosity());
		getPoolWriter().writeCell(this.getUniCount());
		getPoolWriter().writeCell(this.getHomFaoCount());
		getPoolWriter().writeCell(this.getHetFaoCount());

		getPoolWriter().newRow();
	}

	// until debugging the dataGatheres is done, use:
	private double getFaoMeanHeterozygosity() {
		int count = 0;
		double sum = 0;
		// double faoMeanHomozygosity=-1;
		for (Organism o : this) {
			count += 1 * (((OutXingOrganism) o).isFAO());
			sum += ((OutXingOrganism) o).getHeterozigosity()
					* (((OutXingOrganism) o).isFAO());
		}
		return sum / count;
	}

	private double getFaoMeanHomozygosity() {
		int count = 0;
		double sum = 0;
		// double faoMeanHeterozygosity=-1;
		for (Organism o : this) {
			count += 1 * (((OutXingOrganism) o).isFAO());
			sum += ((OutXingOrganism) o).getDeleteriousHomozigosity()
					* (((OutXingOrganism) o).isFAO());
		}
		return sum / count;
	}

	private double getUniMeanHeterozygosity() {
		int count = 0;
		double sum = 0;
		// double uniMeanHomozygosity= -1;
		for (Organism o : this) {
			count += 1 * ((1 - ((OutXingOrganism) o).isFAO()));
			sum += ((OutXingOrganism) o).getHeterozigosity()
					* ((1 - ((OutXingOrganism) o).isFAO()));
		}
		return sum / count;
	}

	private double getUniCount() {
		int count = 0;
		for (Organism o : this) {
			int mod = ((OutXingOrganism) o).getOutXingModifier();
			if (mod == OutXingOrganism.HOMOZYGOT_AA)
				count++;
		}
		return count;
	}

	private double getHomFaoCount() {
		int count = 0;
		for (Organism o : this) {
			int mod = ((OutXingOrganism) o).getOutXingModifier();
			if (mod == OutXingOrganism.HOMOZYGOT_BB)
				count++;
		}
		return count;
	}

	private double getHetFaoCount() {
		int count = 0;
		for (Organism o : this) {
			int mod = ((OutXingOrganism) o).getOutXingModifier();
			if (mod == OutXingOrganism.HETEROZYGOT_AB
					|| mod == OutXingOrganism.HETEROZYGOT_BA)
				count++;
		}
		return count;
	}

	private double getUniMeanHomozygosity() {
		int count = 0;
		double sum = 0;
		// double uniMeanHeterozygosity= -1;
		for (Organism o : this) {
			count += 1 * (1 - (((OutXingOrganism) o).isFAO()));
			sum += ((OutXingOrganism) o).getDeleteriousHomozigosity()
					* (1 - ((OutXingOrganism) o).isFAO());
		}
		return sum / count;
	}

	//
	//
	//

	@ScheduledMethod(start = 2, interval = 1, priority =ScheduleParameters.LAST_PRIORITY)
	public void stepsToExecuteLastPriorityAndNotIncludedInStatistics() {
		demesCreateNewGeneration();
		evictPool();
		changeWeather();
		demesRandomDrift();
	}

	/*
	 * This moves all organisms in pool to random demes and evicts the pool. It
	 * is called after reproduction (8<5).NOTE: this is WRONG. the higher the
	 * priority number, the earlier it is executed!!!
	 */
	// @ScheduledMethod(start = 1, interval = 1, priority = 5)
	public void evictPool() {
		Collection<Organism> organisms = pool.getOrganisms();
		pool.clear();
		for (Organism o : organisms) {
			addToRandomDeme(o);
		}
	}

	/*
	 * if weather change indicator==1, then the weather change MOD will be
	 * included else the max population size of the demes will not change and
	 * remain its carrying capacity
	 */
	// @ScheduledMethod(start = 1, interval = 1, priority = 3)
	public void changeWeather() {
		double tick = DateUtils.getCurrentTick();
		double mod = Params.getDemeMaxPopulationModifier()
				* Math
						.sin(tick * Math.PI
								/ Params.getDemeMaxPopulationPeriod()) + 1;
		if (Params.getWeatherChangeIndicator() == 1) {
			int size = (int) Math.floor(Params.getDemeCarryingCapacity() * mod);

			for (DemeSubContext deme : getDemes()) {
				deme.setDemePopulationCapacity(size);
			}
		}
	}

	/*
	 * This method happens once at the end of the simulation
	 */
	@ScheduledMethod(start = ScheduleParameters.END, priority = ScheduleParameters.LAST_PRIORITY)
	public void endOfSimulation() {
		poolWriter.close();
	}

	// making it last priority causes the Repast aggregate statistics to be
	// performed BEFORE its execution
	// so the population doesn't seem constrained to the demes' carrying
	// capacity:
	// @ScheduledMethod(start = 1, interval = 1, priority =
	// ScheduleParameters.2)
	public void demesRandomDrift() {
		for (DemeSubContext deme : getDemes()) {
			deme.randomDrift();
		}
	}

	public List<DemeSubContext> getDemes() {
		if (demes == null)
			demes = new ArrayList<DemeSubContext>();
		return demes;
	}

	public PoolSubContext getPool() {
		return pool;
	}

	public void setPool(PoolSubContext pool) {
		if (this.pool != null) {
			removeSubContext(this.pool);
		}
		this.pool = pool;
		addSubContext(pool);
	}

	/*
	 * this will also add to the demes
	 */
	public void addDeme(DemeSubContext deme) {
		addSubContext(deme);
	}

	@Override
	public void addSubContext(Context<? extends Organism> subcontext) {
		// this method is implemented because when deserializing structured
		// context the subcontexts get added through here, and not through
		// setPool or addDeme
		if (subcontext instanceof PoolSubContext) {
			this.pool = (PoolSubContext) subcontext;
		} else if (subcontext instanceof DemeSubContext
				&& !getDemes().contains(subcontext)) {
			getDemes().add((DemeSubContext) subcontext);
		} else {
			throw new IllegalArgumentException(
					"Only expected subcontexts of types "
							+ PoolSubContext.class.getName() + " or "
							+ DemeSubContext.class.getName() + ", but got "
							+ subcontext.getClass().getName());
		}
		super.addSubContext(subcontext);
	}

	// while problems on Repast's dataGatheres persist, use poolWriter to write
	// the means of het and hom of F and U
	public CsvWriter poolWriterLazyInitiztion() {
		poolWriter = new CsvWriter("output/" + Params.getJobName() + ".pool."
				+ Params.getJobDate() + ".csv", new String[] { "tick", "count",
				"mean fitness", "mean heterozygosity",
				"mean deleterious homozygosity", "mhetU", "mhomU", "mhetF",
				"mhomF", "uni count", "hom fao count", "het fao count" });
		return poolWriter;
	}

	public CsvWriter getPoolWriter() {
		if (poolWriter == null) {
			poolWriterLazyInitiztion();
		}
		return poolWriter;
	}

	/*
	 * public void setMigrationFitnessCost(double migrationFitnessCost) {
	 * this.migrationFitnessCost = migrationFitnessCost; }
	 * 
	 * public double getMigrationFitnessCost() { return migrationFitnessCost; }
	 * 
	 * public void setInoutXingFitnessCost(double inoutXingFitnessCost) {
	 * this.inoutXingFitnessCost = inoutXingFitnessCost; }
	 * 
	 * public double getInOutXingFitnessCost() { return inoutXingFitnessCost; }
	 * 
	 * public void setHeterozygotCoefficient(double heterozygotCoefficient) {
	 * this.heterozygotCoefficient = heterozygotCoefficient; }
	 * 
	 * public double getHeterozygotCoefficient() { return
	 * heterozygotCoefficient; }
	 * 
	 * public void setDemeCarryingCapacity(int demeCarryingCapacity) {
	 * this.demeCarryingCapacity = demeCarryingCapacity; }
	 * 
	 * public int getDemeCarryingCapacity() { return demeCarryingCapacity; }
	 */

}
